<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>18.3.1-将组件渲染为 HTML 字符串</title>
</head>
<body>
    
</body>
    <script>
        /**
         * 如何将组件类型的虚拟节点渲染为 HTML 字符串
         * - 只需要执行组件的渲染函数取得对应的虚拟 DOM，
         *    再将该虚拟 DOM 渲染为HTML 字符串，
         *    并作为 renderComponentVNode 函数的返回值即可
         * 
         * renderComponentVNode 仅仅展示了渲染组件的最基本原理，仍然存在很多问题
         * - subTree 本身可能是任意类型的虚拟节点，包括组件类型。
         *    因此，我们不能直接使用 renderElementVNode 来渲染它。
         * - 执行 setup 函数时，也应该提供 setupContext 对象。
         *    而执行渲染函数render 时，也应该将其 this 指向 renderContext 对象。
         *    实际上，在组件的初始化和渲染方面，其完整流程与第13章讲解的客户端的渲染流程一致。
         *    例如，也需要初始化 data，也需要得到 setup 函数的执行结果，
         *     并检查 setup 函数的返回值是函数还是 setupState 等。
         * 
        */


        

        // 应该忽略的属性
        const shouldIgnoreProp = ['key', 'ref']

        function renderAttrs(props) {
            let ret = ''
            for (const key in props) {
                if (
                    // 检测属性名称，如果是事件或应该被忽略的属性，则忽略它
                    shouldIgnoreProp.includes(key) ||
                    /^on[^a-z]/.test(key)
                ) {
                    continue
                }
                const value = props[key]
                // 调用 renderDynamicAttr 完成属性的渲染
                ret += renderDynamicAttr(key, value)
            }
            return ret
        }

        // 用来判断属性是否是 boolean attribute
        const isBooleanAttr = (key) =>
            (`itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly` +
            `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` +
            `loop,open,required,reversed,scoped,seamless,` +
            `checked,muted,multiple,selected`).split(',').includes(key)

        // 用来判断属性名称是否合法且安全
        const isSSRSafeAttrName = (key) => !/[>/="'\u0009\u000a\u000c\u0020]/.test(key)

        function renderDynamicAttr(key, value) {
            if (isBooleanAttr(key)) {
                // 对于 boolean attribute，如果值为 false，则什么都不需要渲染，否则只需要渲染 key 即可
                return value === false ? `` : ` ${key}`
            } else if (isSSRSafeAttrName(key)) {
                // 对于其他安全的属性，执行完整的渲染，
                // 注意：对于属性值，我们需要对它执行 HTML 转义操作
                return value === '' ? ` ${key}` : ` ${key}="${escapeHtml(value)}"`
            } else {
                // 跳过不安全的属性，并打印警告信息
                console.warn(
                    `[@vue/server-renderer] Skipped rendering unsafe attribute name: ${key}`
                )
                return ``
            }
        }

        const escapeRE = /["'&<>]/ 
        function escapeHtml(string) {
            const str = '' + string
            const match = escapeRE.exec(str)

            if (!match) {
                return str
            }

            let html = ''
            let escaped
            let index
            let lastIndex = 0
            for (index = match.index; index < str.length; index++) {
                switch (str.charCodeAt(index)) {
                    case 34: // "
                        escaped = '&quot;'
                        break
                    case 38: // &
                        escaped = '&amp;'
                        break
                    case 39: // '
                        escaped = '&#39;'
                        break
                    case 60: // <
                        escaped = '&lt;'
                        break
                    case 62: // >
                        escaped = '&gt;'
                        break
                    default:
                        continue
                }

                if (lastIndex !== index) {
                    html += str.substring(lastIndex, index)
                }

                lastIndex = index + 1
                html += escaped
            }

            return lastIndex !== index ? html + str.substring(lastIndex, index) : html
        }




        
        const VOID_TAGS = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'.split(',')
        // 在不考虑任何边界条件的情况下，实现 renderElementVNode
        function renderElementVNode(vnode){
            // 取出标签名称 tag 和标签属性 props，以及标签的子节点
            const { type: tag, props, children } = vnode
            // 判断是否是 void element
            const isVoidElement = VOID_TAGS.includes(tag)
            // 开始标签的头部
            let ret = `<${tag}`
            // 处理标签属性
            if (props) {
                // 调用 renderAttrs 函数进行严谨处理
                ret += renderAttrs(props)
            }

            // 如果是 void element，则自闭合
            ret += isVoidElement ? `/>` : `>` 
            // 如果是 void element，则直接返回结果，无须处理 children，因为 void element 没有 children
            if (isVoidElement) return ret 

            // 处理子节点
            // 如果子节点的类型是字符串，则是文本内容，直接拼接
            if (typeof children === 'string') {
                ret += children
            } else if (Array.isArray(children)) {
                // 如果子节点的类型是数组，则递归地调用 renderElementVNode 完成渲染
                children.forEach(child => {
                    ret += renderElementVNode(child)
                })
            }

            // 结束标签
            ret += `</${tag}>`

            // 返回拼接好的 HTML 字符串
            return ret
        }

        function renderComponentVNode(vnode) { // 新增
            // 获取 setup 组件选项
            let { type: { setup } } = vnode
            // 执行 setup 函数得到渲染函数 render
            const render = setup()
            // 执行渲染函数得到 subTree，即组件要渲染的内容
            const subTree = render()
            // 调用 renderElementVNode 完成渲染，并返回其结果
            return renderElementVNode(subTree)
        }
        // 组件
        const MyComponent = {  // 新增
            setup() {
                return () => {
                    // 该组件渲染一个 div 标签
                    return {
                        type: 'div',
                        children: 'hello'
                    }
                }
            }
        }

        // 用来描述组件的 VNode 对象
        const CompVNode = { // 新增
            type: MyComponent,
        }


        
        
        // 测试
        console.log(renderComponentVNode(CompVNode))
    </script>
</html>